package tolgahan.albayrak.as3.reflection
{
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;
	
	import tolgahan.albayrak.as3.reflection.converters.TypeConverter;
	
	public final class Type
	{
		public static const Missing:Type = new Type;
		
		private static const converters:Dictionary = new Dictionary;
		private static const classes:Dictionary = new Dictionary;
		private static const nameMap:Dictionary = new Dictionary;
		
		private static const VARIABLE:String = "variable";
		private static const CONSTANT:String = "constant";
		private static const ACCESSOR:String = "accessor";
		private static const METHOD:String = "method";
		private static const FACTORY:String = "factory";
		private static const CONSTRUCTOR:String = "constructor";
		private static const EXTENDSCLASS:String = "extendsClass";
		private static const IMPLEMENTSINTERFACE:String = "implementsInterface";
		
		
		private var _base:Type;
		private var _nameSpace:String;
		private var _localName:String;
		private var _fullName:String;
		private var _isStatic:Boolean;
		private var _isDynamic:Boolean;
		private var _isFinal:Boolean;
		private var _classReference:Class;
		
		private var _staticVariables:Vector.<StaticVariableInfo> = new Vector.<StaticVariableInfo>;
		private var _constants:Vector.<ConstantInfo> = new Vector.<ConstantInfo>;
		private var _staticProperties:Vector.<StaticPropertyInfo> = new Vector.<StaticPropertyInfo>;
		private var _staticMethods:Vector.<StaticMethodInfo> = new Vector.<StaticMethodInfo>;
		
		private var _variables:Vector.<VariableInfo> = new Vector.<VariableInfo>;
		private var _properties:Vector.<PropertyInfo> = new Vector.<PropertyInfo>;
		private var _methods:Vector.<MethodInfo> = new Vector.<MethodInfo>;
		
		private var _extends:*;
		private var _implements:*;
		
		private var _propertyList:Dictionary = new Dictionary;
		
		private var _constructor:ConstructorInfo;
		private var _loadedFromInstance:Boolean;
		
		public function Type()
		{
			_extends = new Vector.<Type>;
			_implements = new Vector.<Type>;
		}
		
		ns_reflection function loadFromInstance(object:Object):Type
		{
			if(!_loadedFromInstance && object)
			{
				_loadedFromInstance = true;
				var xml:XML = describeType(object);
				_constructor = new ConstructorInfo(xml.factory.constructor[0], this, 0);
				_isDynamic = String(xml.@isDynamic[0]) == "true";
				_isFinal = String(xml.@isFinal[0]) == "true";
				_isStatic = String(xml.@isStatic[0]) == "true";
			}
			return this;
		}
		
		private function load(classReference:Class):Type
		{
			_classReference = classReference;
			loadClassInfo(describeType(classReference));
			return this;
		}
		
		private function loadByXML(xml:XML):Type
		{
			loadClassInfo(xml);
			_classReference = getDefinitionByName(_fullName) as Class;
			return this;
		}
		
		private function loadClassInfo(xml:XML):void
		{
			_fullName = String(xml.@name[0]);
			nameMap[_fullName] = this;
			var ary:Array = _fullName.split("::");
			if(ary.length < 2)
			{
				_localName = ary[0];
				
			}else{
				_nameSpace = ary[0];
				_localName = ary[1];
			}

			_isDynamic = String(xml.@isDynamic[0]) == "true";
			_isFinal = String(xml.@isFinal[0]) == "true";
			_isStatic = String(xml.@isStatic[0]) == "true";
			
			for each(var child:XML in xml.children())
			{
				var ln:String = child.localName();
				switch(ln)
				{
					case VARIABLE:
						_staticVariables.push(new StaticVariableInfo(child, this, _staticVariables.length));
						break;
					case CONSTANT:
						_constants.push(new ConstantInfo(child, this, _constants.length));
						break;
					case ACCESSOR:
						var prop:StaticPropertyInfo = loadPropertyInfo(true, child, _staticProperties.length);
						if(prop)
							_staticProperties.push(prop);
						break;
					case METHOD:
						var method:StaticMethodInfo = loadMethodInfo(true, child, _staticMethods.length);
						if(method)
							_staticMethods.push(method);
						break;
					case FACTORY:
						loadFactory(child);
						break;
				}
			}
			_staticVariables.fixed = true;
			_constants.fixed = true;
			_properties.fixed = true;
			_staticMethods.fixed = true;
			_staticProperties.fixed = true;
			_extends.fixed = true;
			_implements.fixed = true;
			_methods.fixed = true;
		}
		
		private function loadFactory(xml:XML):void
		{
			var type:String;
			var _typ:Type;
			for each(var child:XML in xml.children())
			{
				var ln:String = child.localName();
				switch(ln)
				{
					case VARIABLE:
						_variables.push(new VariableInfo(child, this, _variables.length));
						break;
					case ACCESSOR:
						var prop:PropertyInfo = loadPropertyInfo(false, child, _properties.length);
						if(prop)
							_properties.push(prop);
						break;
					case METHOD:
						var method:MethodInfo = loadMethodInfo(false, child, _methods.length);
						if(method)
							_methods.push(method);
						break;
					case CONSTRUCTOR:
						_constructor = new ConstructorInfo(child, this, 0);
						break;
					case EXTENDSCLASS:
						type = String(child.@type[0]);
						if(type == "" || type == "*")
							type = "Object";
						type = type.replace("*", "Object");
						_typ = Type.GetType(getDefinitionByName(type) as Class);
						if(_typ)
							_extends.push(_typ);
						break;
					case IMPLEMENTSINTERFACE:
						type = String(child.@type[0]);
						if(type == "" || type == "*")
							type = "Object";
						type = type.replace("*", "Object");
						_typ = Type.GetType(getDefinitionByName(type) as Class);
						if(_typ)
							_implements.push(_typ);
						break;
				}
			}
			if(!_constructor)
				_constructor = new ConstructorInfo(<root />, this, 0);
		}
		
		private function loadPropertyInfo(staticProp:Boolean, xml:XML, index:uint):*
		{
			var declaredBy:String = xml.@declaredBy[0];
			if(declaredBy != _fullName)
			{
				declaredBy = declaredBy.replace("*", "Object");
				var cr:Class = getDefinitionByName(declaredBy) as Class;
				var _typ:Type = Type.GetType(cr);
				var n:String = xml.@name[0];
				return 	staticProp ?
					_typ.getStaticProperty(n) :
					_typ.getProperty(n);
			}
			return staticProp ? new StaticPropertyInfo(xml, this, index) : new PropertyInfo(xml, this, index);
		}
		
		private function loadMethodInfo(staticMethod:Boolean, xml:XML, index:uint):*
		{
			var declaredBy:String = xml.@declaredBy[0];
			if(declaredBy != _fullName)
			{
				declaredBy = declaredBy.replace("*", "Object");
				var cr:Class = getDefinitionByName(declaredBy) as Class;
				var _typ:Type = Type.GetType(cr);
				var n:String = xml.@name[0];
				return 	staticMethod ?
					_typ.getStaticMethod(n) :
					_typ.getMethod(n);
			}
			return staticMethod ? new StaticMethodInfo(xml, this, index) : new MethodInfo(xml, this, index);
		}
		
		public function get staticVariables():Vector.<StaticVariableInfo>
		{
			return _staticVariables.concat();
		}
		
		public function getStaticVariable(name:String):StaticVariableInfo
		{
			var vi:StaticVariableInfo;
			_staticVariables.some
				(
					function(item:StaticVariableInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function get isDynamic():Boolean
		{
			return _isDynamic;
		}
		
		public function get isFinal():Boolean
		{
			return _isFinal;
		}
		
		public function get isInterface():Boolean
		{
			return _constructor == null;
		}
		
		public function get isMissing():Boolean
		{
			return this == Missing || !this._classReference;
		}
		
		public function get canCreateInstance():Boolean
		{
			return _constructor != null;
		}
		
		public function get constructor():ConstructorInfo
		{
			return _constructor;
		}
		
		public function get baseType():Type
		{
			return _extends.length > 0 ? _extends[0] : null;
		}
		
		public function get baseTypes():Vector.<Type>
		{
			return _extends.concat();
		}
		
		public function implementsInterface(interfaceType:Type):Boolean
		{
			return _implements.indexOf(interfaceType) > -1;
		}
		
		public function get implementedInterfaces():Vector.<Type>
		{
			return _implements.concat();
		}
		
		public function isAssignableFrom(_typ:Type):Boolean
		{
			if(!_typ)
				return false;
			return	(_typ._extends.indexOf(this) > -1) ||
				(_typ._implements.indexOf(this) > -1);
		}
		
		public function isSubClassOf(_typ:Type):Boolean
		{
			if(!_typ)
				return false;
			return 	(_extends.indexOf(_typ) > -1) ||
				(_implements.indexOf(_typ) > -1);
		}
		
		public function get hasStaticVariable():Boolean
		{
			return _staticVariables.length > 0;
		}
		
		public function getStaticVariableAt(index:uint):StaticVariableInfo
		{
			return index < _staticVariables.length ? _staticVariables[index] : null;
		}
		
		public function get constants():Vector.<ConstantInfo>
		{
			return _constants.concat();
		}
		
		public function getConstant(name:String):ConstantInfo
		{
			var vi:ConstantInfo;
			_staticVariables.some
				(
					function(item:ConstantInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function getConstantAt(index:uint):ConstantInfo
		{
			return index < _constants.length ? _constants[index] : null;
		}
		
		public function get hasConstant():Boolean
		{
			return _constants.length > 0;
		}
		
		public function get staticProperties():Vector.<StaticPropertyInfo>
		{
			return _staticProperties.concat();
		}
		
		public function getStaticProperty(name:String):StaticPropertyInfo
		{
			var vi:StaticPropertyInfo;
			_staticProperties.some
				(
					function(item:StaticPropertyInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function get hasStaticProperty():Boolean
		{
			return _staticProperties.length > 0;
		}
		
		public function getStaticPropertyAt(index:uint):StaticPropertyInfo
		{
			return index < _staticProperties.length ? _staticProperties[index] : null;
		}
		
		public function get properties():Vector.<PropertyInfo>
		{
			return _properties.concat();
		}
		
		public function getProperty(name:String):PropertyInfo
		{
			var vi:PropertyInfo;
			_properties.some
				(
					function(item:PropertyInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function get hasProperty():Boolean
		{
			return _properties.length > 0;
		}
		
		public function getPropertyAt(index:uint):PropertyInfo
		{
			return index < _properties.length ? _properties[index] : null;
		}		
		
		public function get methods():Vector.<MethodInfo>
		{
			return _methods.concat();
		}
		
		public function getMethod(name:String):MethodInfo
		{
			var vi:MethodInfo;
			_methods.some
				(
					function(item:MethodInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function get hasMethod():Boolean
		{
			return _methods.length > 0;
		}
		
		public function getMethodAt(index:uint):MethodInfo
		{
			return index < _methods.length ? _methods[index] : null;
		}				
		
		public function getStaticMethod(name:String):StaticMethodInfo
		{
			var vi:StaticMethodInfo;
			_staticMethods.some
				(
					function(item:StaticMethodInfo, ...args):Boolean
					{
						if(item.name == name)
						{
							vi = item;
							return true;
						}
						return false;
					}
				);
			return vi;
		}
		
		public function get hasStaticMethod():Boolean
		{
			return _staticMethods.length > 0;
		}
		
		public function getStaticMethodAt(index:uint):StaticMethodInfo
		{
			return index < _staticMethods.length ? _staticMethods[index] : null;
		}				
		
		public function get isStatic():Boolean
		{
			return _isStatic;
		}
		
		public function get localName():String
		{
			return _localName;
		}
		
		
		public function get nameSpace():String
		{
			return _nameSpace || "";
		}
		
		
		public function get fullName():String
		{
			return _fullName;
		}
		
		private var qn:QName;
		public function get name():QName
		{
			return qn || (qn = new QName(_nameSpace || "", _localName));
		}
		
		
		public function get classReference():Class
		{
			return _classReference;
		}
		
		public function get isPrimitive():Boolean
		{
			return 	this == Primitives.IntType || 
				this == Primitives.DateType || 
				this == Primitives.NumberType ||
				this == Primitives.StringType ||
				this == Primitives.UIntType;
		}
		
		public function get imports():Vector.<Type>
		{
			var v:Vector.<Type> = new Vector.<Type>;
			_staticVariables.forEach
				(
					function(item:TypedMemberInfo, ...args):void
					{
						if(item.type.nameSpace && v.indexOf(item.type) < 0)
							v.push(item.type);
					}
				);
			_variables.forEach
				(
					function(item:TypedMemberInfo, ...args):void
					{
						if(item.type.nameSpace && v.indexOf(item.type) < 0)
							v.push(item.type);
					}
				);
			_constants.forEach
				(
					function(item:TypedMemberInfo, ...args):void
					{
						if(item.type.nameSpace && v.indexOf(item.type) < 0)
							v.push(item.type);
					}
				);
			_properties.forEach
				(
					function(item:TypedMemberInfo, ...args):void
					{
						if(item.type.nameSpace && v.indexOf(item.type) < 0)
							v.push(item.type);
					}
				);
			_staticProperties.forEach
				(
					function(item:TypedMemberInfo, ...args):void
					{
						if(item.type.nameSpace && v.indexOf(item.type) < 0)
							v.push(item.type);
					}
				);
			_methods.forEach
				(
					function(item:MethodInfo, ...args):void
					{
						if(item.returnType && item.returnType.nameSpace && v.indexOf(item.returnType) < 0)
							v.push(item.returnType);
						item.parameters.forEach
						(
							function(itemx:TypedMemberInfo, ...args):void
							{
								if(itemx.type.nameSpace && v.indexOf(itemx.type) < 0)
									v.push(itemx.type);
							}
						);
					}
				);
			
			_staticMethods.forEach
				(
					function(item:MethodInfo, ...args):void
					{
						if(item.returnType && item.returnType.nameSpace && v.indexOf(item.returnType) < 0)
							v.push(item.returnType);
						item.parameters.forEach
						(
							function(itemx:TypedMemberInfo, ...args):void
							{
								if(itemx.type.nameSpace && v.indexOf(itemx.type) < 0)
									v.push(itemx.type);
							}
						);
					}
				);
			
			return v;
		}
		
		public function toString():String
		{
			return (_nameSpace ? _nameSpace + "." : "") + _localName;
		}
		
		public function toClassDefinitionString():String
		{
			var v:Vector.<MemberInfo> = new Vector.<MemberInfo>;
			if(_constants.length)
			{
				v.push(new LabelInfo("// constants"));
				v = v.concat(_constants);
			}
			if(_staticVariables.length)
			{
				v.push(new LabelInfo("// static variables"));
				v = v.concat(_staticVariables);
			}
			if(_staticProperties.length)
			{
				v.push(new LabelInfo("// static properties"));
				v = v.concat(_staticProperties);
			}
			if(_staticMethods.length)
			{
				v.push(new LabelInfo("// static methods"));
				v = v.concat(_staticMethods);
			}
			var t:String = "class";
			if(_constructor)
			{
				v.push(new LabelInfo("// constructor"));
				v.push(_constructor);
				if(_variables.length)
				{
					v.push(new LabelInfo("// variables"));
					v = v.concat(_variables);
				}
				if(_properties.length)
				{
					v.push(new LabelInfo("// properties"));
					v = v.concat(_properties);
				}
				if(_methods.length)
				{
					v.push(new LabelInfo("// methods"));
					v = v.concat(_methods);
				}
			}else{
				t = "interface";
			}
			var imp:Vector.<Type> = imports;
			return "package " + (_nameSpace || "") + "\n{\n" +
				(
					imp.length ?
					(
						"\timport " + 
						imp.join(";\n\timport ") + 
						";\n\n"
					) :
					""
				) +
				"\tpublic " + t + " " + _localName + "\n\t{\n\t\t" +
				v.join("\n\t\t") +
				"\n\t}\n}";
		}		
		
		public static function IsAssignableFrom(toClass:Class, fromClass:Class):Boolean
		{
			return GetType(toClass).isAssignableFrom(GetType(fromClass));
		}
		
		public static function IsSubClassOf(subClass:Class, baseClass:Class):Boolean
		{
			return GetType(subClass).isSubClassOf(GetType(baseClass));
		}
		
		public static function GetTypeByInstance(instance:*):Type
		{
			if(instance)
			{
				var classReference:Class = getDefinitionByName(getQualifiedClassName(instance)) as Class;
				return GetType(classReference).ns_reflection::loadFromInstance(instance);
			}
			return Type.Missing;
		}
		
		public static function GetType(classReference:Class):Type
		{
			return 	classes[classReference] || 
				(
					(
						classes[classReference] = new Type()
					).load(classReference)
				);
		}
		
		public static function GetTypeByName(name:String):Type
		{
			var type:Type = nameMap[name];
			if(!type)
			{
				try
				{
					var classReference:Class = getDefinitionByName(name) as Class;
					if(classReference)
					{
						return GetType(classReference);
					}else{
						
					}
				}
				catch(err:Error)
				{
				}
				type = nameMap[name] = Type.Missing;
			}
			return type;
		}
		
		public static function GetTypeByQName(name:QName):Type
		{
			return GetTypeByName(String(name));
		}
		
		public static function HasConverter(type:Type):Boolean
		{
			return (type in converters);
		}
		
		public static function RegisterTypeConverter(type:Type, converter:TypeConverter):TypeConverter
		{
			return (converters[type] = converter);
		}
		
		public static function GetConverter(type:Type):TypeConverter
		{
			return converters[type];
		}
		
		public static function get ResolvedTypes():Vector.<Type>
		{
			var v:Vector.<Type> = new Vector.<Type>;
			for each(var type:Type in classes)
				v.push(type);
			return v;
		}
		
	}
}